package de.uni_stuttgart.ipvs.ids.communication;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketTimeoutException;
import java.util.Collection;
import java.util.Vector;

/**
 * Part b) Extend the method receiveMessages to return all DatagramPackets that
 * were received during the given timeout.
 * 
 * Also implement unpack() to conveniently convert a Collection of
 * DatagramPackets containing ValueResponseMessages to a collection of
 * MessageWithSource objects.
 * 
 */

/**
 * A non blocking receiver, offering the possibility to receiving a certain
 * amount of messages within a defined time interval.
 * 
 * @author Team 3/4 Elkhan Dadashov, Ahmed Labib, Dhaval A Shah, Patrick Stortz
 */
public class NonBlockingReceiver {

	/** The DatagramSocket used by this receiver. */
	protected DatagramSocket socket;
	/** The name identifying this receiver. */
	protected String name;

	/**
	 * Instantiates a new non blocking receiver.
	 * 
	 * @param socket
	 *            the DatagramSocket the receiver will listen to.
	 */
	public NonBlockingReceiver(DatagramSocket socket) {
		this.name = "Receiver @ " + socket.getLocalPort();
		this.socket = socket;
	}

	/** @return the name identifying this receiver. */
	public String getName() {
		return name;
	}

	/**
	 * Listens to the socket and receives messages.<br>
	 * The receiver will either listen until the amount of messages in parameter
	 * expectedMessages have been arrived or the time [ms] in parameter
	 * timeoutMillis is over.
	 * 
	 * @param timeoutMillis
	 *            The maximum time in ms the receiver will wait for all messages
	 *            (time this method call will block).
	 * @param expectedMessages
	 *            The maximum number of messages to wait for.
	 * @return A Vector with all received messages.
	 * @throws IOException
	 *             Thrown if listening for messages fails.
	 */
	public Vector<DatagramPacket> receiveMessages(int timeoutMillis,
			int expectedMessages) throws IOException {

		// System.out.println(this.getName() + " | Waiting for "
		// + expectedMessages + " packets...");// DEBUG

		// calculate the latest time this method call has to return
		long overAllEndTime = System.currentTimeMillis() + timeoutMillis;

		// start listening for the messages
		Vector<DatagramPacket> receivedPackets = new Vector<DatagramPacket>(
				expectedMessages);
		for (int i = 0; i < expectedMessages; i++) {
			try {
				// create the packet
				byte[] buffer = new byte[1024];
				DatagramPacket receivedPacket = new DatagramPacket(buffer,
						buffer.length);
				// calculate the timeout for this packet (remaining time until
				// end time)
				long receiveStartTime = System.currentTimeMillis();
				int timeOut = (int) (overAllEndTime - receiveStartTime);
				// check if we have time left
				if (timeOut > 0) {
					// set the timeout, wait for the packet and add it to the
					// vector
					socket.setSoTimeout(timeOut);
					socket.receive(receivedPacket);
					receivedPackets.add(receivedPacket);
				} else {
					// no time left, return from the method call
					System.err.println(this.getName()
							+ " | Overall timeout passed!");// DEBUG
					break;
				}
			} catch (SocketTimeoutException e) {
				// no time left, return from the method call
				System.err.println(this.getName()
						+ " | Overall timeout passed!");// DEBUG
				break;
			}
		}

		// System.out.println(this.getName() + " | Received "
		// + receivedPackets.size() + " of " + expectedMessages
		// + " expected messages!");// DEBUG

		return receivedPackets;

	}

	/**
	 * Converts a collection of DatagramPackets containing DatagramPacket to a
	 * collection of MessageWithSource objects.
	 * 
	 * @param packetCollection
	 *            a collection containing DatagramPacket objects
	 * @return a collection containing the MessageWithSource objects
	 * @throws IOException
	 *             Thrown if one of the DatagramPacket objects contains invalid
	 *             data.
	 * @throws ClassNotFoundException
	 *             Thrown if one of the DatagramPacket objects can not be
	 *             serialized.
	 */
	public static <T> Collection<MessageWithSource<T>> unpack(
			Collection<DatagramPacket> packetCollection) throws IOException,
			ClassNotFoundException {

		Collection<MessageWithSource<T>> messages = new Vector<MessageWithSource<T>>(
				packetCollection.size());
		// loop through the packets
		for (DatagramPacket packet : packetCollection) {
			@SuppressWarnings("unchecked")
			// generic types are lost during compilation, we cannot check
			// against it
			// get the object in their data
			T message = (T) getObjectFromMessage(packet);
			// add the object to the collection
			messages.add(new MessageWithSource<T>(packet.getSocketAddress(),
					message));

		}
		return messages;
	}

	/**
	 * Deserializes an object from a DatagramPacket.
	 * 
	 * @param packet
	 *            The DatagramPacket with the serialized object in its data.
	 * @return The deserialized object
	 * @throws IOException
	 *             Thrown if there are errors during reading the stream.
	 * @throws ClassNotFoundException
	 *             Thrown if the DatagramPacket object can not be serialized.
	 */
	protected static Object getObjectFromMessage(DatagramPacket packet)
			throws IOException, ClassNotFoundException {
		// get the byte stream
		ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(
				packet.getData());
		// get the object stream
		ObjectInputStream objectInputStream = new ObjectInputStream(
				byteArrayInputStream);
		// read the object from the stream
		Object object = objectInputStream.readObject();
		// close the steam
		objectInputStream.close();
		return object;
	}

}
